<?php
/**
 * Created by PhpStorm.
 * User: terry
 * Date: 2018/10/12
 * Time: 上午11:01
 */

namespace AdminBundle\RBAC;


use AdminBundle\Entity\AuthAssignment;
use AdminBundle\Entity\AuthItem;
use AdminBundle\Entity\AuthItemChild;
use AdminBundle\Entity\AuthRule;
use Doctrine\Bundle\DoctrineBundle\Registry;
use InvalidArgumentException;

class DbManager extends BaseManager
{
    private $doctrine;

    /**
     * @var AuthItem[] all auth items (name => AuthItem)
     */
    protected $items;

    /**
     * @var AuthRule[] all auth rules (name => AuthRule)
     */
    protected $rules;

    /**
     * @var array auth item parent-child relationships (childName => list of parents)
     */
    protected $parents;

    private $temp;

    function __construct(Registry $doctrine)
    {
        $this->doctrine = $doctrine;
    }

    /**
     * Returns the named auth item.
     * @param string $name the auth item name.
     * @return Item the auth item corresponding to the specified name. Null is returned if no such item.
     */
    public function getItem($name)
    {
        $item = $this->getAuthItem($name);

        return $this->populateItem($item);
    }

    protected function getAuthItem($name)
    {
        if (empty($name)) {
            return null;
        }

        if (!empty($this->items[$name])) {
            return $this->items[$name];
        }

        return $this->doctrine
            ->getRepository(AuthItem::class)
            ->find($name);
    }

    /**
     * Returns the items of the specified type.
     * @param integer $type the auth item type (either [[Item::TYPE_ROLE]] or [[Item::TYPE_PERMISSION]]
     * @return Item[] the auth items of the specified type.
     */
    protected function getItems($type)
    {
        $result = $this->doctrine
            ->getRepository(AuthItem::class)
            ->findBy(array('type' => $type));

        $items = array();
        foreach ($result as $o) {
            $items[$o->getName()] = $this->populateItem($o);
        }

        return $items;
    }

    protected function populateItem($item)
    {
        if ($item == null) {
            return null;
        }

        $class = $item->getType() == Item::TYPE_PERMISSION ? Permission::className() : Role::className();

//        if (($data = @unserialize($item->getData())) === false) {
//            $data = null;
//        }

        $result = new $class();
        $result->name = $item->getName();
        $result->type = $item->getType();
        $result->description = $item->getDescription();
//        $result->ruleName = $item->getRuleName();
//        $result->data = $data;
        $result->menuId = $item->getMenuId();
        $result->createdAt = $item->getCreatedAt();
        $result->updatedAt = $item->getUpdatedAt();

        return $result;
    }

    protected function addItem(Item $item)
    {
        $datetime = new \DateTime();

        if ($item->createdAt === null) {
            $item->createdAt = $datetime;
        }
        if ($item->updatedAt === null) {
            $item->updatedAt = $datetime;
        }
        if ($item->data === null) {
            $item->data = serialize($item->data);
        }

        if ($this->doctrine->getRepository(AuthItem::class)->find($item->name)) {
            throw new \Exception('Item already exists!');
        }

        $authItem = new AuthItem($item->name);

        $authItem->setType($item->type);
        $authItem->setDescription($item->description);
//        $authItem->setRuleName($item->ruleName);
//        $authItem->setData($item->data);
        $authItem->setCreatedAt($item->createdAt);
        $authItem->setUpdatedAt($item->updatedAt);
        $authItem->setMenuId($item->menuId ?: 0);

        $em = $this->doctrine->getManager();
        $em->persist($authItem);
        $em->flush();
    }

    protected function addRule($rule)
    {
        $datetime = new \DateTime();

        if ($rule->createdAt === null) {
            $rule->createdAt = $datetime;
        }
        if ($rule->updatedAt === null) {
            $rule->updatedAt = $datetime;
        }

        $authRule = new AuthRule($rule->name);
        $authRule->setData(serialize($rule));
        $authRule->setCreatedAt($rule->createdAt);
        $authRule->setUpdatedAt($rule->updatedAt);

        $em = $this->doctrine->getManager();
        $em->persist($authRule);
        $em->flush();
    }

    /**
     * Removes an auth item from the RBAC system.
     * @param Item $item the item to remove
     * @return boolean whether the role or permission is successfully removed
     * @throws \Exception if data validation or saving fails (such as the name of the role or permission is not unique)
     */
    protected function removeItem($item)
    {
        $this->temp = $item;

        $this->doctrine->getManager()->transactional(function($em) {
            // 同步删除AuthAssignment, AuthItemChild
            $em->createQuery('DELETE FROM AdminBundle\Entity\AuthAssignment a WHERE a.itemName = :name')
                ->setParameter('name', $this->temp->name)
                ->execute();

            $em->createQuery('DELETE FROM AdminBundle\Entity\AuthItemChild ic WHERE ic.parent = :name')
                ->setParameter('name', $this->temp->name)
                ->execute();
            $em->createQuery('DELETE FROM AdminBundle\Entity\AuthItemChild ic WHERE ic.child = :name')
                ->setParameter('name', $this->temp->name)
                ->execute();

            $em->createQuery('DELETE FROM AdminBundle\Entity\AuthItem i WHERE i.name = :name')
                ->setParameter('name', $this->temp->name)
                ->execute();
        });

        return true;
    }

    /**
     * Removes a rule from the RBAC system.
     * @param Rule $rule the rule to remove
     * @return boolean whether the rule is successfully removed
     * @throws \Exception if data validation or saving fails (such as the name of the rule is not unique)
     */
    protected function removeRule($rule)
    {
        // TODO: 同步删除AuthItem.

        return true;
    }

    /**
     * Updates an auth item in the RBAC system.
     * @param string $name the name of the item being updated
     * @param Item $item the updated item
     * @return boolean whether the auth item is successfully updated
     * @throws \Exception if data validation or saving fails (such as the name of the role or permission is not unique)
     */
    protected function updateItem($name, $item)
    {
        $authItem = $this->getAuthItem($name);
        if (!$authItem) {
            return false;
        }

        // TODO 如果修改了name，同步更新其他表

        $authItem->setName($item->name);
        $authItem->setDescription($item->description);
//        $authItem->setRuleName($item->ruleName);
//        $authItem->setData($item->data === null ? null : serialize($item->data));
        $authItem->setMenuId($item->menuId ?: 0);
        $authItem->setUpdatedAt(new \DateTime());

        $em = $this->doctrine->getManager();
        $em->persist($authItem);
        $em->flush();

        return true;
    }

    /**
     * Updates a rule to the RBAC system.
     * @param string $name the name of the rule being updated
     * @param Rule $rule the updated rule
     * @return boolean whether the rule is successfully updated
     * @throws \Exception if data validation or saving fails (such as the name of the rule is not unique)
     */
    protected function updateRule($name, $rule)
    {
        // TODO: 如果修改了name，同步更新AuthItem.

        $authRule = $this->doctrine
            ->getRepository(AuthRule::class)
            ->find($name);

        if ($authRule === null) {
            return false;
        }

        $authRule->setName($rule->name);
        $authRule->setData(serialize($rule));
        $authRule->setUpdatedAt(new \DateTime());

        $em = $this->doctrine->getManager();
        $em->persist($authRule);
        $em->flush();

        return true;
    }

    /**
     * Checks if the user has the specified permission.
     * @param string|integer $userId the user ID. This should be either an integer or a string representing
     * the unique identifier of a user. See [[\yii\web\User::id]].
     * @param string $permissionName the name of the permission to be checked against
     * @param array $params name-value pairs that will be passed to the rules associated
     * with the roles and permissions assigned to the user.
     * @return boolean whether the user has the specified permission.
     * @throws InvalidArgumentException if $permissionName does not refer to an existing permission
     */
    public function checkAccess($userId, $permissionName, $params = [])
    {
        $assignments = $this->getAssignments($userId);
//        $this->loadFromCache();
//        $this->checkAccessFromCache();

        return $this->checkAccessRecursive($permissionName, $params, $assignments);
    }

    /**
     * Returns all role assignment information for the specified user.
     * @param string|integer $userId the user ID (see [[\yii\web\User::id]])
     * @return Assignment[] the assignments indexed by role names. An empty array will be
     * returned if there is no role assigned to the user.
     */
    public function getAssignments($userId)
    {
        if (empty($userId)) {
            return array();
        }

        $result = $this->doctrine
            ->getRepository(AuthAssignment::class)
            ->findBy(array('userId' => $userId));

        $assignments = array();

        foreach ($result as $obj) {
            $assignments[$obj->getItemName()] = array(
                'userId' => $obj->getUserId(),
                'roleName' => $obj->getItemName(),
                'createdAt' => $obj->getCreatedAt()
            );
        }

        return $assignments;
    }

    /**
     * 递归查询某角色是否具有指定权限
     *
     * @param $itemName
     * @param $params
     * @param $assignments
     * @return bool
     */
    public function checkAccessRecursive($itemName, $params, $assignments)
    {
        if (($item = $this->getItem($itemName)) === null) {
            return false;
        }

        // TODO 规则验证

        // 如果在用户与权限（角色）的分配表中匹配到了该权限且规则验证也ok了就说明该用户有权限了
        if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
            return true;
        }

        // 如果目前来看该用户没有该操作的权限那么就从权限和角色的上下级关联表中找到该权限的父级，然后递归的检查该用户在该父级里有没有权限，如果父级有权限了那么该权限也有了
        $parents = $this->doctrine
            ->getRepository(AuthItemChild::class)
            ->findBy(array('child' => $itemName));

        foreach ($parents as $parent) {
            if ($this->checkAccessRecursive($parent->getParent(), $params, $assignments)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns the roles that are assigned to the user via [[assign()]].
     * Note that child roles that are not assigned directly to the user will not be returned.
     * @param string|integer $userId the user ID (see [[\yii\web\User::id]])
     * @return Role[] all roles directly or indirectly assigned to the user. The array is indexed by the role names.
     */
    public function getRolesByUser($userId)
    {
        // TODO: Implement getRolesByUser() method.
    }

    /**
     * Returns all permissions that the specified role represents.
     * @param string $roleName the role name
     * @return Permission[] all permissions that the role represents. The array is indexed by the permission names.
     */
    public function getPermissionsByRole($roleName)
    {
        // TODO: Implement getPermissionsByRole() method.
    }

    /**
     * Returns all permissions that the user has.
     * @param string|integer $userId the user ID (see [[\yii\web\User::id]])
     * @return Permission[] all permissions that the user has. The array is indexed by the permission names.
     */
    public function getPermissionsByUser($userId)
    {
        // TODO: Implement getPermissionsByUser() method.
        if (empty($userId)) {
            return [];
        }

        $childrenList = $this->getChildrenList();
        $assignments = $this->doctrine
            ->getRepository(AuthAssignment::class)
            ->findBy(array('userId' => $userId));
        $result = [];
        foreach ($assignments as $assignment) {
            $this->getChildrenRecursive($assignment->getItemName(), $childrenList, $result);
        }

        if (empty($result)) {
            return [];
        }

        $items = $this->doctrine
            ->getRepository(AuthItem::class)
            ->findBy([
                'type' => Item::TYPE_PERMISSION,
                'name' => array_keys($result)
            ]);

        $permissions = [];
        foreach ($items as $row) {
            $permissions[$row->getName()] = $this->populateItem($row);
        }

        return $permissions;
    }

    /**
     * Returns the rule of the specified name.
     * @param string $name the rule name
     * @return null|Rule the rule object, or null if the specified name does not correspond to a rule.
     */
    public function getRule($name)
    {
        // TODO: Implement getRule() method.
    }

    /**
     * Returns all rules available in the system.
     * @return Rule[] the rules indexed by the rule names
     */
    public function getRules()
    {
        // TODO: Implement getRules() method.
    }

    /**
     * Adds an item as a child of another item.
     * @param Item $parent
     * @param Item $child
     * @return bool
     * @throws InvalidArgumentException if the parent-child relationship already exists or if a loop has been detected.
     */
    public function addChild($parent, $child)
    {
        if ($parent->name === $child->name) {
            throw new InvalidArgumentException("Cannot add '{$parent->name}' as a child of itself.");
        }

        if ($parent instanceof Permission && $child instanceof Role) {
            throw new InvalidArgumentException("Cannot add a role as a child of a permission.");
        }

        if ($this->detectLoop($parent, $child)) {
            throw new InvalidArgumentException("Cannot add '{$child->name}' as a child of '{$parent->name}'. A loop has been detected.");
        }

        $auth_item_child = new AuthItemChild($parent->name, $child->name);

        $em = $this->doctrine->getManager();
        $em->persist($auth_item_child);
        $em->flush();

        return true;
    }

    /**
     * Removes a child from its parent.
     * Note, the child item is not deleted. Only the parent-child relationship is removed.
     * @param Item $parent_name
     * @param Item $child_name
     * @return boolean whether the removal is successful
     */
    public function removeChild($parent_name, $child_name)
    {
        $auth_item_child = $this->doctrine
            ->getRepository(AuthItemChild::class)
            ->findOneBy(array('parent' => $parent_name, 'child' => $child_name));

        if ($auth_item_child === null) {
            throw new InvalidArgumentException("Can not remove the child.");
        }

        $em = $this->doctrine->getManager();
        $em->remove($auth_item_child);
        $em->flush();

        return true;
    }

    /**
     * Removed all children form their parent.
     * Note, the children items are not deleted. Only the parent-child relationships are removed.
     * @param Item $parent
     * @return boolean whether the removal is successful
     */
    public function removeChildren($parent)
    {
        // TODO: Implement removeChildren() method.
    }

    /**
     * Returns a value indicating whether the child already exists for the parent.
     * @param Item $parent
     * @param Item $child
     * @return boolean whether `$child` is already a child of `$parent`
     */
    public function hasChild($parent, $child)
    {
        // TODO: Implement hasChild() method.
    }

    /**
     * Returns the child permissions and/or roles.
     * @param string $name the parent name
     * @return Item[] the child permissions and/or roles
     */
    public function getChildren($name)
    {
        $qb = $this->doctrine->getManager()->createQueryBuilder();
        $result = $qb->select('i')
            ->from('AdminBundle\Entity\AuthItem', 'i')
            ->join(
                'AdminBundle\Entity\AuthItemChild',
                'ic',
                \Doctrine\ORM\Query\Expr\Join::WITH,
                'i.name = ic.child')
            ->where('ic.parent = :parent')
            ->setParameter('parent', $name)
            ->getQuery()
            ->getResult();

        $children = array();

        foreach ($result as $o) {
            $children[$o->getName()] = $this->populateItem($o);
        }

        return $children;
    }

    /**
     * Assigns a role to a user.
     *
     * @param Role $role
     * @param string|integer $userId the user ID (see [[\yii\web\User::id]])
     * @return Assignment the role assignment information.
     * @throws \Exception if the role has already been assigned to the user
     */
    public function assign($role, $userId)
    {
        $assignment = new Assignment();
        $assignment->roleName = $role->name;
        $assignment->userId = $userId;
        $assignment->createdAt = new \DateTime();

        $auth_assignment = new AuthAssignment($assignment->roleName, $assignment->userId);
        $auth_assignment->setCreatedAt($assignment->createdAt);

        $em = $this->doctrine->getManager();
        $em->persist($auth_assignment);
        $em->flush();

        return $assignment;
    }

    /**
     * Revokes a role from a user.
     * @param Role $role
     * @param string|integer $userId the user ID (see [[\yii\web\User::id]])
     * @return boolean whether the revoking is successful
     */
    public function revoke($role, $userId)
    {
        if (empty($userId)) {
            return false;
        }

        $assignment = $this->doctrine
            ->getRepository(AuthAssignment::class)
            ->find(array('itemName' => $role->name, 'userId' => $userId));

        $em = $this->doctrine->getManager();
        $em->remove($assignment);
        $em->flush();

        return true;
    }

    /**
     * Revokes all roles from a user.
     * @param mixed $userId the user ID (see [[\yii\web\User::id]])
     * @return boolean whether the revoking is successful
     */
    public function revokeAll($userId)
    {
        // TODO: Implement revokeAll() method.
    }

    /**
     * Returns the assignment information regarding a role and a user.
     * @param string|integer $userId the user ID (see [[\yii\web\User::id]])
     * @param string $roleName the role name
     * @return null|Assignment the assignment information. Null is returned if
     * the role is not assigned to the user.
     */
    public function getAssignment($roleName, $userId)
    {
        // TODO: Implement getAssignment() method.
    }

    /**
     * Removes all authorization data, including roles, permissions, rules, and assignments.
     */
    public function removeAll()
    {
        // TODO: Implement removeAll() method.
    }

    /**
     * Removes all permissions.
     * All parent child relations will be adjusted accordingly.
     */
    public function removeAllPermissions()
    {
        // TODO: Implement removeAllPermissions() method.
    }

    /**
     * Removes all roles.
     * All parent child relations will be adjusted accordingly.
     */
    public function removeAllRoles()
    {
        // TODO: Implement removeAllRoles() method.
    }

    /**
     * Removes all rules.
     * All roles and permissions which have rules will be adjusted accordingly.
     */
    public function removeAllRules()
    {
        // TODO: Implement removeAllRules() method.
    }

    /**
     * Removes all role assignments.
     */
    public function removeAllAssignments()
    {
        // TODO: Implement removeAllAssignments() method.
    }

    /**
     * Checks whether there is a loop in the authorization item hierarchy.
     *
     * @param Item $parent the parent item
     * @param Item $child the child item to be added to the hierarchy
     * @return boolean whether a loop exists
     */
    protected function detectLoop($parent, $child)
    {
        if ($child->name === $parent->name) {
            return true;
        }

        foreach ($this->getChildren($child->name) as $grandchild) {
            if ($this->detectLoop($parent, $grandchild)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns the children for every parent.
     * @return array the children list. Each array key is a parent item name,
     * and the corresponding array value is a list of child item names.
     */
    protected function getChildrenList()
    {
        $children = $this->doctrine->getRepository(AuthItemChild::class)->findAll();
        $parents = [];
        foreach ($children as $row) {
            $parents[$row->getParent()][] = $row->getChild();
        }

        return $parents;
    }

    /**
     * Recursively finds all children and grand children of the specified item.
     * @param string $name the name of the item whose children are to be looked for.
     * @param array $childrenList the child list built via [[getChildrenList()]]
     * @param array $result the children and grand children (in array keys)
     */
    protected function getChildrenRecursive($name, $childrenList, &$result)
    {
        if (isset($childrenList[$name])) {
            foreach ($childrenList[$name] as $child) {
                $result[$child] = true;
                $this->getChildrenRecursive($child, $childrenList, $result);
            }
        }
    }
}